package net.zoneland.knowledge.filters;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.commons.lang3.time.StopWatch;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import org.springframework.web.util.WebUtils;

import com.branchitech.utils.SystemHelper;
import com.google.common.collect.ImmutableSet;

import net.zoneland.knowledge.constant.Constants;
import net.zoneland.knowledge.dao.PerformanceMonitorMapper;
import net.zoneland.knowledge.enums.VisitTypeEnum;
import net.zoneland.knowledge.model.PerformanceMonitor;
import net.zoneland.knowledge.model.security.OperationContextHolder;
import net.zoneland.knowledge.model.security.OperationPrincipal;
import net.zoneland.knowledge.utils.LoginUtils;

/**
 * 过滤器用于记录耗时.
 * @author bcc
 */
public class PerformanceMonitorFilter extends OncePerRequestFilter {

    /**
     * 日志.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(PerformanceMonitorFilter.class);

    /**
     * 下载contentType类型.
     */
    private static final ImmutableSet<String> DEFAULT_DOWNLOAD_CONTENT_TYPE = ImmutableSet.of(
            "application/vnd.ms-excel",//.xls
            "application/msexcel",//.xls
            "application/cvs",//.cvs
            MediaType.APPLICATION_OCTET_STREAM_VALUE,//.*（ 二进制流，不知道下载文件类型）
            "application/x-xls",//.xls
            "application/msword",//.doc
            MediaType.TEXT_PLAIN_VALUE,//.txt
            "application/x-gzip"//.gz
    );

    /**
     * 未知常量.
     */
    private static final String UNKNOWN = "[UNKNOWN]";

    private final PerformanceMonitorMapper performanceMonitorMapper;

    public PerformanceMonitorFilter(final PerformanceMonitorMapper performanceMonitorMapper) {
        this.performanceMonitorMapper = performanceMonitorMapper;
    }

    @Override
    protected void doFilterInternal(final HttpServletRequest request,
                                    final HttpServletResponse response,
                                    final FilterChain filterChain)
            throws ServletException, IOException {

        final String requestMethod = request.getMethod();
        final boolean shouldWrapMethod = StringUtils.equalsIgnoreCase(requestMethod, HttpMethod.PUT.name())
                || StringUtils.equalsIgnoreCase(requestMethod, HttpMethod.POST.name());

        final boolean isFirstRequest = !isAsyncDispatch(request);

        final boolean shouldWrapRequest = isFirstRequest
                && !(request instanceof ContentCachingRequestWrapper) && shouldWrapMethod;
        final HttpServletRequest requestToUse = shouldWrapRequest ? new ContentCachingRequestWrapper(request) : request;

        final boolean shouldWrapResponse = !(response instanceof ContentCachingResponseWrapper) && shouldWrapMethod;
        final HttpServletResponse responseToUse = shouldWrapResponse ? new ContentCachingResponseWrapper(response) : response;

        final StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        Exception t = null;
        try {
            filterChain.doFilter(requestToUse, responseToUse);
        } catch (Exception e) {
            t = e;
            throw e;
        } finally {
            stopWatch.stop();
            doSavePerformanceMonitorLog(requestToUse, responseToUse, stopWatch.getTime(), t);
        }
    }

    /**
     * 保存日志.
     * @param request 请求
     * @param response 响应
     * @param useTime 耗时
     * @param exception 异常
     */
    private void doSavePerformanceMonitorLog(final HttpServletRequest request,
                                             final HttpServletResponse response,
                                             final long useTime,
                                             final Exception exception) {
        if (isAsyncStarted(request)) {
            copyResponse(response);
            return;
        }
        try {
            final String requestUri = request.getRequestURI();
            final String requestIp = LoginUtils.getIpAddr(request);
            final String requestParams = getRequestParams(request);
            final String requestString = isUpload(request) ? StringUtils.EMPTY : getRequestString(request);
            final String responseString = isDownload(response) ? StringUtils.EMPTY : getResponseString(response);
            final String userAgent = request.getHeader("User-Agent");

            final PerformanceMonitor record = new PerformanceMonitor();

            record.setId(UUID.randomUUID().toString());
            record.setMethod(request.getMethod());
            record.setRequestUri(requestUri);
            record.setCost(useTime);
            record.setIp(requestIp);
            if (null != exception) {
                record.setErrorInfo(ExceptionUtils.getStackTrace(exception));
            }
            record.setRequestBody(requestString);
            record.setRequestParam(requestParams);
            record.setResponse(responseString);
            record.setCreateTime(LocalDateTime.now());

            final String terminalType = userAgent == null
                    ? VisitTypeEnum.PC.toString()
                    : (SystemHelper.isMobileDevice(userAgent) ? VisitTypeEnum.MOA.toString() : VisitTypeEnum.PC.toString());

            record.setTerminalType(terminalType);

            //设置当前登录用户
            final OperationPrincipal principal = OperationContextHolder.getPrincipal();
            if (null != principal) {
                record.setUserId(principal.getLogonId());
            }
            performanceMonitorMapper.insert(record);
        } catch (Throwable e) {
            LOGGER.error("got an exception when saving access log", e);
        } finally {
            copyResponse(response);
        }
    }

    private String getRequestParams(final HttpServletRequest request) {
        final String requestParamsStr = request.getParameterMap().entrySet()
                .stream()
                .map(entry -> {
                    final String name = entry.getKey();
                    final String value = Optional.ofNullable(entry.getValue())
                            .map(Stream::of)
                            .orElseGet(Stream::empty)
                            .collect(Collectors.joining(Constants.STRING_SPLIT));
                    return name + "=" + value;
                })
                .collect(Collectors.joining("&"));
        if (StringUtils.equalsIgnoreCase(request.getContentType(), MediaType.APPLICATION_FORM_URLENCODED_VALUE)) {
            try {
                return URLDecoder.decode(requestParamsStr, StandardCharsets.UTF_8.name());
            } catch (UnsupportedEncodingException ignored) {
            }
        }
        return StringUtils.EMPTY;
    }

    private boolean isUpload(final HttpServletRequest request) {
        final String contentType = request.getHeader(HttpHeaders.CONTENT_TYPE);
        return StringUtils.containsIgnoreCase(contentType, MediaType.MULTIPART_FORM_DATA_VALUE);
    }

    private boolean isDownload(final HttpServletResponse response) {
        final String contentType = response.getContentType();
        return DEFAULT_DOWNLOAD_CONTENT_TYPE.stream().anyMatch(it -> StringUtils.equalsIgnoreCase(it, contentType));
    }

    private String getRequestString(final HttpServletRequest request) {
        final ContentCachingRequestWrapper wrapper = WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
        if (wrapper != null) {
            try {
                final byte[] buf = wrapper.getContentAsByteArray();
                return new String(buf, wrapper.getCharacterEncoding()).replaceAll("\n|\r", "");
            } catch (UnsupportedEncodingException e) {
                return UNKNOWN;
            }
        }
        return StringUtils.EMPTY;
    }

    private String getResponseString(final HttpServletResponse response) {
        final ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        if (wrapper != null) {
            try {
                final byte[] buf = wrapper.getContentAsByteArray();
                return new String(buf, wrapper.getCharacterEncoding()).replaceAll("\n|\r", "");
            } catch (UnsupportedEncodingException e) {
                return UNKNOWN;
            }
        }
        return StringUtils.EMPTY;
    }

    private void copyResponse(final HttpServletResponse response) {
        final ContentCachingResponseWrapper wrapper = WebUtils.getNativeResponse(response, ContentCachingResponseWrapper.class);
        if (wrapper != null) {
            try {
                wrapper.copyBodyToResponse();
            } catch (IOException ignored) {
                // 感觉这里记录日志作用不大，没法处理，也不能记录现场
                LOGGER.error("copyResponse error", ignored);
            }
        }
    }

}
